<!DOCTYPE html>
<html>
<head>
    <style>
        .pass, .fail, .running {
            height: 30px;
            line-height: 30px;
            color: white;
            display: block;
            border: 1px solid black;
            margin: 3px;
            padding: 3px;
        }

        .pass { background-color: green; }
        .fail { background-color: red; }
        .running { background-color: blue; }
    </style>
</head>
<body>
    <script>
        const bindMethodsForTest = (method, maxInvocations) => {
            let failures = 0;

            const span = document.createElement("span");
            span.innerHTML = `Running: ${method}`;
            span.setAttribute("class", "running");
            span.id = method;

            const pass = () => {
                if (failures > 0) {
                    return;
                }

                ++pass.invocationCount;

                if (pass.invocationCount > maxInvocations) {
                    fail(`setTimeout callback triggered more than ${maxInvocations} time(s)`);
                } else if (pass.invocationCount === maxInvocations) {
                    const span = document.getElementById(method);
                    span.innerHTML = `Pass! ${method}`;
                    span.setAttribute("class", "pass");
                }
            };

            const fail = message => {
                const span = document.getElementById(method);
                span.innerHTML = `Fail! ${method}: ${message}`;
                span.setAttribute("class", "fail");
                ++failures;
            };

            document.body.appendChild(span);
            pass.invocationCount = 0;

            return [pass, fail];
        };

        (() => {
            const [pass, fail] = bindMethodsForTest("setTimeout triggered immediately", 1);
            setTimeout(pass, 0);
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest("setTimeout triggered after 1 second", 1);
            setTimeout(pass, 1000);
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest("setTimeout triggered with string callback", 1);
            window.setTimeoutStringCallback = pass;

            setTimeout("setTimeoutStringCallback();", 500);
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest("setTimeout cancelled before execution", 1);
            pass();

            const id = setTimeout(pass, 1000);
            clearTimeout(id);
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest("setTimeout triggered with callback that throws", 1);

            // The error should be reported, but should not halt execution of other JavaScript.
            setTimeout(() => {
                setTimeout(pass, 500);
                throw new Error();
            }, 500);
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest("setTimeout callback unaffected by cancelling another timer", 1);

            const id = setTimeout(pass, 1000);
            clearTimeout(id + 1);
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest("setTimeout callback triggered with extra arguments", 1);

            setTimeout((integer, string, date) => {
                if (integer !== 12389) {
                    fail(`Expected first argument to be the integer 12389, got ${integer}`);
                } else if (string !== "foo") {
                    fail(`Expected second argument to be the string foo, got ${string}`);
                } else if (!(date instanceof Date)) {
                    fail(`Expected third argument to be a date, got ${date}`);
                }
                pass();
            }, 600, 12389, "foo", new Date());
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest("setInterval repeats callback until cancelled", 5);

            const id = setInterval(pass, 500);
            setTimeout(() => clearInterval(id), 2600);
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest("setInterval cancelled before execution", 1);
            pass();

            const id = setInterval(pass, 1000);
            clearInterval(id);
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest("setInterval cancelled during execution", 1);

            const id = setInterval(() => {
                pass();
                clearInterval(id);
            }, 500);
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest("setInterval triggered with string callback", 6);
            window.setIntervalStringCallback = pass;

            const id = setInterval("setIntervalStringCallback();", 300);
            setTimeout(() => clearInterval(id), 1900);
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest("setInterval triggered with callback that throws", 3);
            let callCount = 0;

            const id = setInterval(() => {
                ++callCount;
                pass();

                if (callCount === 2) {
                    throw new Error();
                } else if (callCount === 3) {
                    clearInterval(id);
                }
            }, 500);
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest("setInterval callback unaffected by cancelling another timer", 1);

            const id = setInterval(() => {
                pass();
                clearInterval(id);
            }, 1000);

            clearInterval(id + 1);
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest("setInterval callback triggered with extra arguments", 1);

            const id = setInterval((integer, string, date) => {
                if (integer !== 12389) {
                    fail(`Expected first argument to be the integer 12389, got ${integer}`);
                } else if (string !== "foo") {
                    fail(`Expected second argument to be the string foo, got ${string}`);
                } else if (!(date instanceof Date)) {
                    fail(`Expected third argument to be a date, got ${date}`);
                }

                pass();
                clearInterval(id);
            }, 600, 12389, "foo", new Date());
        })();
    </script>
</body>

</html>
